跳到主要内容

AST 是什么

学习别人开源项目时无意中看到了一个 ast 包,遂好奇这是啥,发现是一个 语法树包

ast 包是用于探索 Go 包的语法树,它能执行静态分析、代码检查,元编程,以及任何需要对 Go 源码结构化解释的需求。

首先来看下什么是抽象语法树?

抽象语法树的概念

AST 是 Abstract syntax trees 的缩写,在计算机科学中,抽象语法树(AST),或者仅仅是语法树,是用编程语言编写的源代码的抽象语法结构的树状表示。树的每个节点都表示源代码中出现的一个构造。

可以参考这篇文章 何为语法树

通过这个 Parser produces the (beautiful) syntax tree 网站可以在线观察如何生成语法树的

大多数编译器和解释器都使用AST作为源代码的内部表示,AST通常会省略语法树中的分号、换行字符、白空格、大括号、方括号和圆括号等。

下面我们来看看计算机是如何将一份代码文件转换成 AST 的。

  Source Code

Lexer

Tokens

Parser

AST

首先,计算机先用一个词法分析器(Lexer)对文本(Source Code)进行词法分析,生成 Token。一般接下来是将它传给一个解析器,然后检索生成 AST。

Lexing 词法分析器

Lexing 又名词法分析器,词法分析器用来将字符序列转换为单词(Token)。词法分析主要是完成:

  • 对源程序的代码进行从左到右的逐行扫描,识别出各个单词,从而确定单词的类型;
  • 将识别出的单词转换为统一的机内表示——词法单元(Token)形式。

Token 词法单元

token 是一种类型 Map 的 key/value 形式,它由 <种别码,属性值> 组成,种别码 就是类型、属性值 就是值。例如下述代码中:

package main
const s = "foo"

转换为 token 之后:

PACKAGE(package) 
IDENT(main)
CONST(const)
IDENT(s)
ASSIGN(=)
STRING("foo")
EOF()

通过上述的例子我们知道,词法分析器的主要作用是将代码按照单词的维度进行 “分割”,注意该处的单词可能与传统意义上的单词不一样,例如,语言中的关键字是一个单词、赋值 = 同样是一个单词、甚者换行符 \n 也是一个单词,完成词法分析后,我们可以得到该代码文件的 token 数组,数组中的每一个元素为一个词法单元(Token)。

看下 go/token/token.go 的源代码可知,token 就是一堆定义好的枚举类型,对于每种类型的字面值都有对应的 token。

// The list of tokens.
const (
// Special tokens
ILLEGAL Token = iota
EOF
COMMENT

literal_beg
// Identifiers and basic type literals
// (these tokens stand for classes of literals)
IDENT // main
INT // 12345
FLOAT // 123.45
IMAG // 123.45i
CHAR // 'a'
STRING // "abc"
literal_end
......
)

Parser 语法分析器

语法分析器的作用是进行语法检查、并构建由输入的单词(Token)组成的数据结构,语法分析的分析方法一般分为 自顶向下自底向上 两种。

整个流程

Lexing:给定一个包含组成语言的所有标记(符号/词)的表,lexer 将标记放在一起并相应地标记。

Syntax Analysis(语法分析器):每种语言都有它必须遵循的特定的语法规则。给定词法分析器的输出,语法分析器将构建一个表示该语言的操作和语法的解析树。例如,下面的标记 “2,+,2” 将被分组为解析树,如下所示(用于加法的简单解析树):

Syntax Analysis / Semantic Analysis(语法分析/语义分析):编译器将验证解析树,以确保它在语言的上下文中有意义,并通常会向解析树添加语义和上下文,以及删除不必要的构造。这就是创建 AST 的地方。上述表达式的 AST 可以在下面看到( 简单的加法 AST):

注意 AST 如何将结构标识为二进制表达式,并相应地标记左操作数、操作符和右操作数

通过上述的叙述中,我们知道通过 “词法分析” 和 “语义分析” 我们便可以得到 AST,例如:

package main

import (
"fmt"
)

func main() {
fmt.Printf("Hello, Golang\n")
}

上述代码的 AST 为:

0  *ast.File {
1 . Doc: nil
2 . Package: foo:1:1
3 . Name: *ast.Ident {
4 . . NamePos: foo:1:9
5 . . Name: "main"
6 . . Obj: nil
7 . }
8 . Decls: []ast.Decl (len = 2) {
9 . . 0: *ast.GenDecl {
10 . . . Doc: nil
11 . . . TokPos: foo:3:1
12 . . . Tok: import
13 . . . Lparen: foo:3:8
14 . . . Specs: []ast.Spec (len = 1) {
15 . . . . 0: *ast.ImportSpec {
16 . . . . . Doc: nil
17 . . . . . Name: nil
18 . . . . . Path: *ast.BasicLit {
19 . . . . . . ValuePos: foo:4:2
20 . . . . . . Kind: STRING
21 . . . . . . Value: "\"fmt\""
22 . . . . . }
23 . . . . . Comment: nil
24 . . . . . EndPos: -
25 . . . . }
26 . . . }
27 . . . Rparen: foo:5:1
28 . . }
29 . . 1: *ast.FuncDecl {
30 . . . Doc: nil
31 . . . Recv: nil
32 . . . Name: *ast.Ident {
33 . . . . NamePos: foo:7:6
34 . . . . Name: "main"
35 . . . . Obj: *ast.Object {
36 . . . . . Kind: func
37 . . . . . Name: "main"
38 . . . . . Decl: *(obj @ 29)
39 . . . . . Data: nil
40 . . . . . Type: nil
41 . . . . }
42 . . . }
43 . . . Type: *ast.FuncType {
44 . . . . Func: foo:7:1
45 . . . . Params: *ast.FieldList {
46 . . . . . Opening: foo:7:10
47 . . . . . List: nil
48 . . . . . Closing: foo:7:11
49 . . . . }
50 . . . . Results: nil
51 . . . }
52 . . . Body: *ast.BlockStmt {
53 . . . . Lbrace: foo:7:13
54 . . . . List: []ast.Stmt (len = 1) {
55 . . . . . 0: *ast.ExprStmt {
56 . . . . . . X: *ast.CallExpr {
57 . . . . . . . Fun: *ast.SelectorExpr {
58 . . . . . . . . X: *ast.Ident {
59 . . . . . . . . . NamePos: foo:8:2
60 . . . . . . . . . Name: "fmt"
61 . . . . . . . . . Obj: nil
62 . . . . . . . . }
63 . . . . . . . . Sel: *ast.Ident {
64 . . . . . . . . . NamePos: foo:8:6
65 . . . . . . . . . Name: "Printf"
66 . . . . . . . . . Obj: nil
67 . . . . . . . . }
68 . . . . . . . }
69 . . . . . . . Lparen: foo:8:12
70 . . . . . . . Args: []ast.Expr (len = 1) {
71 . . . . . . . . 0: *ast.BasicLit {
72 . . . . . . . . . ValuePos: foo:8:13
73 . . . . . . . . . Kind: STRING
74 . . . . . . . . . Value: "\"Hello, Golang\\n\""
75 . . . . . . . . }
76 . . . . . . . }
77 . . . . . . . Ellipsis: -
78 . . . . . . . Rparen: foo:8:30
79 . . . . . . }
80 . . . . . }
81 . . . . }
82 . . . . Rbrace: foo:9:1
83 . . . }
84 . . }
85 . }
86 . Scope: *ast.Scope {
87 . . Outer: nil
88 . . Objects: map[string]*ast.Object (len = 1) {
89 . . . "main": *(obj @ 35)
90 . . }
91 . }
92 . Imports: []*ast.ImportSpec (len = 1) {
93 . . 0: *(obj @ 15)
94 . }
95 . Unresolved: []*ast.Ident (len = 1) {
96 . . 0: *(obj @ 58)
97 . }
98 . Comments: nil
99 }

我们看到,即使非常简单的代码生成AST也非常复杂。那么我们得到了抽象语法树可以做什么呢?

AST 的常见的几个用途

常见的几种用途:

1、代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等

  • 如使用语言的 Lint 工具对代码错误或风格的检查,发现一些潜在的错误
  • IDE 的错误提示、格式化、高亮、自动补全等

2、代码混淆压缩

  • UglifyJS2 等

3、优化变更代码,改变代码结构使达到想要的结构

  • 代码打包工具 webpack、rollup 等等
  • CommonJS、AMD、CMD、UMD 等代码规范之间的转化
  • CoffeeScript、TypeScript、JSX 等转化为原生 Javascript

References